iT邦幫忙

2024 iThome 鐵人賽

DAY 2
1
自我挑戰組

學習網頁開發系列 第 2

Python 描述器

  • 分享至 

  • xImage
  •  

內文

描述器是什麼

Descriptors let objects customize attribute lookup, storage, and deletion.

以上是Python Documentation對描述器的解釋,也是最清楚、最直觀的解釋。直翻的話就是「描述器允許物件自訂屬性的查找、儲存和刪除行為」。

描述器可以想像成有特定功能的一個類別物件,它基本包含了__get____set____delete__三個方法對應上述提到的查找、儲存和刪除。

Get method

class MyDescriptor: # 描述器
    def __get__(self, instance, instanceType=None):
        return "Hello World"

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
print(obj.attr) # Output: Hello World

當編譯器執行obj.attr時,他實際上會跑attr.__get__(obj, MyClass),所以會回傳Hello World回來。

呼叫__get__ method以取得attribute的值。 這個方法除了self外還會有兩個參數:

  1. instance: 從中訪問attribute的類別實體
  2. instanceType: 擁有attribute的類別

如果__get__是由類別呼叫,則instance為None.

Set method

加上 __set__ 來編輯 attribute:

class MyDescriptor: # 描述器
    def __init__(self):
        self._name = "Hello World"
    
    def __get__(self, instance, instancetype=None):
        return self._name
    
    def __set__(self, instance, value):
        if isinstance(value, str):
            self._name = value
        else:
            raise ValueError("Name must be a string")

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
print(obj.attr) # Output: Hello World
obj.attr = "Good Night"
print(obj.attr) # Output: Good Night
obj.attr = 123 # ValueError: Name must be a string

從這個案例中可以看到描述器的第一個好處:對attribute的設定增加自己的邏輯。
obj.attr = "Good Night"為例,實際執行的流程為attr.__set__(obj, "Good Night")。這時候我們自己加了輸入值屬性的檢測,因此obj.attr = 123的123就會因為屬性不合被擋住。

Delete method

再加上__delete__

class MyDescriptor: # 描述器
    def __init__(self):
        self._name = "Hello World"
    
    def __get__(self, instance, instancetype=None):
        return self._name
    
    def __set__(self, instance, value):
        if isinstance(value, str):
            self._name = value
        else:
            raise ValueError("Name must be a string")
    
    def __delete__(self, instance):
        raise AttributeError("This attribute cannot be deleted")

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
del obj.attr # AttributeError: "This attribute cannot be deleted"

編譯器執行del obj.attr時,實際上執行att.__delete__(obj)。而這裡設定__delete__會丟出一個錯誤,並不會真的刪掉目標attribute。

Non-data descriptor 和 Data descriptor

只有__get__的為non-data descriptor。除__get__還有__set____delete__的為data descriptor。

Attribute的呼叫順序

當我們呼叫一個attribute時,編譯器會按照此實體的__mro__找尋相應的值。

MRO 代表 Method Resolution Order,是python 中類別決定attrubute值為何的順序。

  1. 用__getattribute__依__mro__尋找該attribute的定義。
  2. 先看是否有data descriptor,有的話就用,沒有的話繼續。
  3. 再看是否在instance dictionary內。沒有的話再繼續。
  4. 接著看是否有non-data descriptor。一樣沒有再繼續。
  5. 最後都都沒有的話,丟給__getattr__處理。並可能提報AttributeError

以下為例:

class A:
    def __getattr__(self, name):
        return f"{name} not found in A, but handled by __getattr__"

class B(A):
    dd_1 = 123  # Regular class attribute

    def __init__(self):
        self.instance_attr = "Instance attribute in B"

b = B()
print(b.dd_1)  # Directly from B's class dictionary
print(b.instance_attr)  # From instance dictionary of b
print(b.some_random_attr)  # Handled by __getattr__ of class A

為什麼需要描述器

統一管理不同實體如何使用attributes

將描述器拆出後,不但可以將attribute的管理邏輯封裝,需要時還可以重複使用這個邏輯。在維護與事後調整上也較清楚容易。

延遲屬性評估/計算

描述器很適合拿來延遲計算某些數值,直到被呼叫到時再計算,而不是先計算好然後佔住部分記憶體。

沒有用描述器的範例:

class DataAnalysis:
    def __init__(self, data):
        self.data = data
        self._result = self._analyze_data()

    def _analyze_data(self):
        # Simulate a time-consuming analysis
        return sum(self.data) / len(self.data)

# 答案在實體化時就會被計算好,並儲存下來。就算之後沒用到也一樣。

有用描述器的範例:

class LazyProperty:
    def __init__(self, function):
        self.function = function
        self.attribute_name = f"_{function.__name__}"

    def __get__(self, obj, objtype=None):
        if not hasattr(obj, self.attribute_name):
            setattr(obj, self.attribute_name, self.function(obj))
        return getattr(obj, self.attribute_name)

class DataAnalysis:
    def __init__(self, data):
        self.data = data

    @LazyProperty
    def result(self):
        # Simulate a time-consuming analysis
        return sum(self.data) / len(self.data)

analysis = DataAnalysis([10, 20, 30, 40])
print(analysis.result)  # result 這時候被呼叫到,計算才會開始並回傳。由於__get__中有setattr,因此會同時將答覆存在obj的__dict__中,成為一個新的實體attribute。
print(analysis.result)  # result 再次被呼叫。但實體__dict__內現在有result,因此不用再次計算,可以直接拿到答案。

規範型別與值

如同上面提過的範例,我們可以在描述器內檢測輸入值是否符合型別要求。
如以下:

class TypeChecked:
    def __init__(self, expected_type, attribute_name):
        self.expected_type = expected_type
        self.attribute_name = attribute_name

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.attribute_name} must be of type {self.expected_type}")
        obj.__dict__[self.attribute_name] = value

class Person:
    name = TypeChecked(str, 'name')
    age = TypeChecked(int, 'age')

# 這樣可以保證 Attributes 的型別與大小符合我們的想像。之後也比較不會有bug。

描述器範例

常見的裝飾器classmethodstaticmethod都是描述器的一種,而且是non-data descriptor。不過兩者實際上都是用C寫得。假如純用Python來寫得話,classmethod會長這樣:

import functools

class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            # This code path was added in Python 3.9
            # and was deprecated in Python 3.11.
            return self.f.__get__(cls, cls)
        return MethodType(self.f, cls)

Reference

  1. https://docs.python.org/3/howto/descriptor.html#primer
  2. https://stackoverflow.com/questions/48537906/how-do-keyword-arguments-interact-with-model-django?rq=1
  3. https://stackoverflow.com/questions/3798835/understanding-get-and-set-and-python-descriptors
  4. https://djangostars.com/blog/python-descriptors/
  5. https://georgexyz.com/python-descriptor-django-model.html

上一篇
用 Docker 建立 PG Container 連結到本地的 Django
下一篇
git 協作流程
系列文
學習網頁開發13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言